/* (c) https://github.com/MontiCore/monticore */
package de.monticore.tf.ruletranslation;

import com.google.common.collect.Lists;
import de.monticore.ast.ASTNode;
import de.monticore.expressions.expressionsbasis.ExpressionsBasisMill;
import de.monticore.expressions.expressionsbasis._ast.ASTExpression;
import de.monticore.literals.mccommonliterals._ast.ASTBooleanLiteral;
import de.monticore.literals.mccommonliterals._ast.ASTStringLiteral;
import de.monticore.literals.mcjavaliterals._ast.ASTIntLiteral;
import de.monticore.types.mcbasictypes._ast.ASTConstantsMCBasicTypes;
import de.monticore.types.mcbasictypes._ast.ASTMCPrimitiveType;
import de.monticore.types.mcbasictypes._ast.ASTMCQualifiedName;
import de.monticore.types.mcsimplegenerictypes._ast.ASTMCBasicGenericType;
import de.monticore.umlstereotype._ast.ASTStereoValue;
import de.monticore.umlstereotype._ast.ASTStereotype;
import de.se_rwth.commons.Names;
import de.se_rwth.commons.Splitters;
import de.se_rwth.commons.logging.Log;
import de.monticore.tf.odrules.ODRulesMill;
import de.monticore.tf.odrules._ast.*;
import de.monticore.tf.odrules._parser.ODRulesParser;
import de.monticore.tf.odrules.util.ODRuleStereotypes;
import de.monticore.tf.odrules.util.Util;
import de.monticore.tf.rule2od.Variable2AttributeMap;
import de.monticore.tf.tfcommons._ast.*;
import de.monticore.tf.tfcommons._visitor.TFCommonsVisitor2;
import de.monticore.tf.ast.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static de.se_rwth.commons.StringTransformations.capitalize;
import static de.monticore.tf.ruletranslation.Position.*;

@SuppressWarnings("unused")
public abstract class Rule2ODVisitor implements TFCommonsVisitor2 {

  // The transformation rule in object diagram syntax generated by this visitor
  protected Rule2ODState state;


  public Rule2ODVisitor(Variable2AttributeMap variable2Attributes, Map<ASTNode, ASTNode> parents) {
    super();
    this.state = new Rule2ODState(variable2Attributes,parents);
  }
  
  public Rule2ODVisitor(Rule2ODState state){
    this.state = state;
  }


  /**
   * @param name the name of an object that has been created by this visitor
   * @return the object with this name if existent, null otherwise
   */
  protected ASTODObject getLhsObjectByName(String name) {
    for (ASTODObject o : Util.getAllODObjects(state.getLhs())) {
      if (name.equals(o.getName())) {
        return o;
      }
    }
    return null;
  }

  /**
   * @param name the name of an object that has been created by this visitor
   * @return the object with this name if existent, null otherwise
   */
  protected ASTODObject getRhsObjectByName(String name) {
    for (ASTODObject o : Util.getAllODObjects(state.getRhs())) {
      if (name.equals(o.getName())) {
        return o;
      }
    }
    return null;
  }

  /**
   * This method will add an ASTODObject to the LHS of the rule.
   * If the hierarchyStack is empty, it will just add it to the
   * lhs definition. If the hierarchyStack contains an element,
   * it will add the object as an inner link of the object in the stack.
   * @param object the object
   */
  protected void addObjectToLHS(ASTODObject object) {
    if (state.getHierarchyLHS().isEmpty()) {
      state.getLhs().addODObject(object);
    }
    else {
      ASTODInnerLinkBuilder builder = ODRulesMill.oDInnerLinkBuilder();
      builder.setODObject(object);
      state.getHierarchyLHS().peek().addInnerLinks(builder.build());
    }
  }

  protected void addObjectToRHS(ASTODObject object) {
    if (state.getHierarchyRHS().isEmpty()) {
      state.getRhs().addODObject(object);
    }
    else {
      ASTODInnerLinkBuilder builder = ODRulesMill.oDInnerLinkBuilder();
      builder.setODObject(object);
      state.getHierarchyRHS().peek().addInnerLinks(builder.build());
    }
  }

  @Override
  public void visit(ASTTFWhere node) {
    state.getGenRule().setConstraint(node.getConstraint().deepClone());
  }

  @Override
  public void visit(ASTTFAssignments node) {
    for (ASTAssign assign : node.getAssignList()) {
      ASTAssignment assignment = ODRulesMill.assignmentBuilder().uncheckedBuild();
      assignment.setLhs(assign.getVariable());
      assignment.setRhs(assign.getValue().deepClone());
      state.getGenRule().addAssignment(assignment);
    }
  }

  @Override
  public void visit(ASTTFFolding node) {
    for (de.monticore.tf.tfcommons._ast.ASTFoldingSet f : node.getFoldingSetList()) {
      state.getGenRule().addFoldingSet(transfromFoldingSet(f));
    }
  }

  @Override
  public void endVisit(ASTTFRule node){
    for(String key : state.getVariable2Attributes().getV2a().keySet()){
      String value =  state.getNameGen().getNameForElement(state.getVariable2Attributes().getObject(key));
      String attr_value = state.getVariable2Attributes().getAttributeName(key);
      if (state.getVariable2Attributes().isList(key)) {
        attr_value = attr_value.replace("s.", "sList.");
      }
      value += ".get" + capitalize(attr_value).replace(".","().");
      if(!value.endsWith(")")) {
        value += "()";
      }
      if(!value.startsWith("$")){
        value = "m." +  value;
      }
      try {
        ASTAssignment assignment = ODRulesMill.assignmentBuilder().uncheckedBuild();
        assignment.setLhs(key);
        assignment.setRhs(new ODRulesParser().parse_StringExpression(value).get());
        state.getGenRule().getAssignmentList().add(0,assignment);      }
      catch (IOException e) {
        Log.error("0xF0901: Invalid Java Expression used in Transformation Rule", e);
      }
    }
  }

  @Override
  public void visit(ASTTFDo node) {
    state.getGenRule().setDoBlock(node.getMCJavaBlock().deepClone());
  }

  private de.monticore.tf.odrules._ast.ASTFoldingSet transfromFoldingSet(de.monticore.tf.tfcommons._ast.ASTFoldingSet f) {
    de.monticore.tf.odrules._ast.ASTFoldingSet s = ODRulesMill.foldingSetBuilder().uncheckedBuild();
    s.setObjectNamesList(new ArrayList<>());
    s.getObjectNamesList().addAll(f.getObjectNamesList());
    return s;
  }

  protected void handleLHS(IPattern node, ASTODObject o_lhs) {
    doHandle(node, o_lhs);
    if (state.isNegative()) {
      ASTStereotype not = ODRulesMill.stereotypeBuilder().uncheckedBuild();
      ASTStereoValue value = ODRulesMill.stereoValueBuilder().uncheckedBuild();
      value.setName(ODRuleStereotypes.NOT);
      not.addValues(value);

      o_lhs.setStereotype(not);
    }
  }

  protected void handleRHS(IPattern node, ASTODObject o_rhs) {
    doHandle(node, o_rhs);
  }

  private void doHandle(IPattern node, ASTODObject obj) {
    obj.setName(state.getNameGen().getNameForElement(node, state.getParents()));
    List<String> qType =
            Splitters.DOT.splitToList(node._getTFElementType().getName());
    ASTMCBasicGenericType type = ODRulesMill.mCBasicGenericTypeBuilder().uncheckedBuild();
    type.setNameList(qType);
    obj.setType(type);
  }

  protected void handleListLHS(IList node, ITFObject listElement) {
    String objectName = state.getNameGen().getNameForElement(listElement, node);
    ASTODObject object = getLhsObjectByName(objectName);
    doHandleList(node, object);
  }

  protected void handleListRHS(IList node, ITFObject listElement) {
    String objectName = state.getNameGen().getNameForElement(listElement, node);
    ASTODObject object = getRhsObjectByName(objectName);
    doHandleList(node, object);
  }

  private void doHandleList(IList node, ASTODObject object) {
    if (node.isPresentSchemaVarName()) {
      object.setName(node.getSchemaVarName());
    }
    ASTStereotype list = ODRulesMill.stereotypeBuilder().uncheckedBuild();
    ASTStereoValue value = ODRulesMill.stereoValueBuilder().uncheckedBuild();
    value.setName(ODRuleStereotypes.LIST);
    list.addValues(value);

    object.setStereotype(list);
  }

  /**
   * Creates an ASTODObject for the given list node
   * and puts it on the hierarchy stack.
   * @param node the list node
   */
  protected void handleList(IList node) {
    ASTODObject objectLhs = ODRulesMill.oDObjectBuilder().uncheckedBuild();
    ASTODObject objectRhs = ODRulesMill.oDObjectBuilder().uncheckedBuild();

    String objectName = state.getNameGen().getNameForElement(node);
    objectLhs.setName(objectName);
    objectRhs.setName(objectName);

    // set stereotype to <<list>>
    ASTStereotype list = ODRulesMill.stereotypeBuilder().uncheckedBuild();
    ASTStereoValue value = ODRulesMill.stereoValueBuilder().uncheckedBuild();
    value.setName(ODRuleStereotypes.LIST);
    list.addValues(value);
    objectLhs.setStereotype(list);
    objectRhs.setStereotype(list);

    // set type to IList
    List<String> qType = Splitters.DOT.splitToList("de.monticore.tf.ast.IList");
    ASTMCBasicGenericType type = ODRulesMill.mCBasicGenericTypeBuilder().uncheckedBuild();
    type.setNameList(qType);
    objectLhs.setType(type);
    objectRhs.setType(type);

    addObjectToLHS(objectLhs);
    state.getHierarchyLHS().push(objectLhs);
    addObjectToRHS(objectRhs);
    state.getHierarchyRHS().push(objectRhs);
  }

  /**
   * Creates an ASTODObject for the given optional node
   * and puts it on the hierarchy stack.
   * @param node the optional node
   */
  protected void handleOpt(IOptional node) {
    ASTODObject objectLhs = ODRulesMill.oDObjectBuilder().uncheckedBuild();
    ASTODObject objectRhs = ODRulesMill.oDObjectBuilder().uncheckedBuild();

    String objectName = state.getNameGen().getNameForElement(node);
    objectLhs.setName(objectName);
    objectRhs.setName(objectName);

    // set stereotype to <<optional>>
    ASTStereotype opt = ODRulesMill.stereotypeBuilder().uncheckedBuild();
    ASTStereoValue value = ODRulesMill.stereoValueBuilder().uncheckedBuild();
    value.setName(ODRuleStereotypes.OPTIONAL);
    opt.addValues(value);
    objectLhs.setStereotype(opt);
    objectRhs.setStereotype(opt);

    // set type to IOptional
    List<String> qType = Splitters.DOT.splitToList("de.monticore.tf.ast.IOptional");
    ASTMCBasicGenericType type = ODRulesMill.mCBasicGenericTypeBuilder().uncheckedBuild();
    type.setNameList(qType);
    objectLhs.setType(type);
    objectRhs.setType(type);

    addObjectToLHS(objectLhs);
    state.getHierarchyLHS().push(objectLhs);
    addObjectToRHS(objectRhs);
    state.getHierarchyRHS().push(objectRhs);
  }

  protected boolean isOnLHS() {
    return (state.getPosition().equals(LHS) || state.getPosition().equals(BOTH));
  }

  protected boolean isOnRHS() {
    return (state.getPosition().equals(RHS) || state.getPosition().equals(BOTH));
  }

  protected ASTExpression createQualifiedNameExpression(List<String> parts) {
    try {
      Optional<ASTExpression> exp = new ODRulesParser()
          .parse_StringExpression(
              Names.getQualifiedName(parts));
      if (exp.isPresent()) {
        return exp.get();
      }
      else {
        Log.error(
            "0xF0006: " + Names.getQualifiedName(parts) + " cannot be treated as an expression");
        return ODRulesMill.nameExpressionBuilder().uncheckedBuild();
      }
    }
    catch (IOException e) {
      Log.error(
          "0xF0004: " + Names.getQualifiedName(parts) + " cannot be treated as an expression");
      return ODRulesMill.nameExpressionBuilder().uncheckedBuild();
    }
  }

  protected ASTODLink createLHSComposition(ITFObject parent, ITFElement child, String roleName, String genericType, boolean attrIsIterated, boolean attrIsOptional) {
    ITFObject component = child instanceof IReplacement ?
        (ITFObject) ((IReplacement) child).getLhs() :
        (ITFObject) child.getTFElement();
    // optionals & lists only group elements, they do not represent elements themselves.
    // in order to create links, we need to get the actual element of the optional
    component = getActualElement(component);
    return createLink(parent, roleName, genericType, component, attrIsIterated, attrIsOptional);
  }

  protected ASTODLink createRHSComposition(ITFObject parent, ITFElement child, String roleName, String genericType,
                                           boolean attrIsIterated, boolean attrIsOptional) {
    ITFObject component = child instanceof IReplacement ?
        (ITFObject) ((IReplacement) child).getRhs() :
        (ITFObject) child.getTFElement();
    // optionals & lists only group elements, they do not represent elements themselves.
    // in order to create links, we need to get the actual element of the optional
    component = getActualElement(component);
    return createLink(parent, roleName, genericType, component, attrIsIterated, attrIsOptional);
  }

  private ITFObject getActualElement(ITFObject component) {
    if (component instanceof IOptional || component instanceof  IList) {
      component = (ITFObject) component.getTFElement();
      return getActualElement(component);
    } else {
      return component;
    }
  }

  protected ASTODLink createLinkInsertAt(ITFObject parent, String roleName, String genericType, ITFObject component, String insertType, boolean attrIsIterated, boolean attrIsOptional) {
    ASTODLink composition = createLink(parent, roleName, genericType, component, attrIsIterated, attrIsOptional);
    ASTStereotype stereotype = composition.getStereotype();
    ASTStereoValue stereoValueType = ODRulesMill.stereoValueBuilder().uncheckedBuild();
    stereoValueType.setName("insertType");
    stereoValueType.setContent(insertType);
    stereotype.addValues(stereoValueType);
    return composition;
  }
  protected ASTODLink createLinkInsertAt(ITFObject parent, String roleName, String genericType, ITFObject component, String insertType,
                                         ITFObject insertAt, boolean attrIsIterated, boolean attrIsOptional) {
    ASTODLink composition = createLinkInsertAt(parent, roleName, genericType, component, insertType, attrIsIterated, attrIsOptional);
    ASTStereoValue stereoValueAt = ODRulesMill.stereoValueBuilder().uncheckedBuild();
    stereoValueAt.setName("insertAt");
    stereoValueAt.setContent(state.getNameGen().getNameForElement(getActualElement(insertAt)));
    composition.getStereotype().addValues(stereoValueAt);
    return composition;
  }

  protected ASTODLink createLink(ITFObject parent, String roleName, String genericType,
                                 ITFObject component, boolean attrIsIterated, boolean attrIsOptional){
    ASTODLink composition = ODRulesMill.oDLinkBuilder().uncheckedBuild();
    composition.setRightRole(roleName);
    List<String> lhs_compositeName = Splitters.DOT.splitToList(state.getNameGen().getNameForElement(parent, state.getParents()));
    ASTMCQualifiedName lhs_compositeQName = ODRulesMill.mCQualifiedNameBuilder().uncheckedBuild();
    lhs_compositeQName.setPartsList(lhs_compositeName);
    composition.addLeftReferenceName(lhs_compositeQName);
    List<String> lhs_componentName =  Splitters.DOT.splitToList(state.getNameGen().getNameForElement(component,state.getParents()));
    ASTMCQualifiedName lhs_componentQName = ODRulesMill.mCQualifiedNameBuilder().uncheckedBuild();
    lhs_componentQName.setPartsList(lhs_componentName);
    composition.addRightReferenceName(lhs_componentQName);
    ASTCardinality cardinality = ODRulesMill.cardinalityBuilder().uncheckedBuild();
    cardinality.setOptional(attrIsOptional && !attrIsIterated);
    cardinality.setOne(!attrIsOptional && !attrIsIterated);
    cardinality.setOneToMany(!attrIsOptional && attrIsIterated);
    cardinality.setMany(attrIsOptional && attrIsIterated);
    composition.setAttributeCardinality(cardinality);
    if(!genericType.isEmpty()) {
      ASTStereotype stereotype = ODRulesMill.stereotypeBuilder().uncheckedBuild();
      ASTStereoValue stereoValueType = ODRulesMill.stereoValueBuilder().uncheckedBuild();
      stereoValueType.setName("genericType");
      stereoValueType.setContent(genericType);
      stereotype.addValues(stereoValueType);
      composition.setStereotype(stereotype);
    }
    return composition;
  }

  protected ASTODAttribute createAttributeForBoolean(String name, ASTExpression value, boolean isOptional) {
    ASTODAttribute attribute = ODRulesMill.oDAttributeBuilder().uncheckedBuild();
    attribute.setName(name);
    ASTMCPrimitiveType primitiveType = ODRulesMill.mCPrimitiveTypeBuilder().uncheckedBuild();
    primitiveType.setPrimitive(ASTConstantsMCBasicTypes.BOOLEAN);
    attribute.setMCType(primitiveType);
    attribute.setSingleValue(value);
    attribute.setAttributeCardinality(ODRulesMill.cardinalityBuilder().uncheckedBuild());
    attribute.getAttributeCardinality().setOptional(isOptional);
    attribute.getAttributeCardinality().setOne(!isOptional);
    return attribute;
  }

  protected ASTExpression createExpressionForBoolean(int astConstant) {
    ASTBooleanLiteral booleanLiteral = ODRulesMill.booleanLiteralBuilder().uncheckedBuild();
    booleanLiteral.setSource(astConstant);
    return ExpressionsBasisMill.literalExpressionBuilder().setLiteral(booleanLiteral).build();
  }
  
  protected ASTExpression createExpressionForInt(int astConstant) {
    ASTIntLiteral intLiteral = ODRulesMill.intLiteralBuilder().uncheckedBuild();
    intLiteral.setSource(""+astConstant);
    return ExpressionsBasisMill.literalExpressionBuilder().setLiteral(intLiteral).build();
  }

  protected ASTODAttribute createAttributeForString(String name, boolean isOptional) {
    ASTODAttribute attribute = ODRulesMill.oDAttributeBuilder().uncheckedBuild();
    ASTMCBasicGenericType type = ODRulesMill.mCBasicGenericTypeBuilder().uncheckedBuild();
    type.setNameList(Splitters.DOT.splitToList("String"));
    attribute.setMCType(type);
    attribute.setName(name);
    attribute.setAttributeCardinality(ODRulesMill.cardinalityBuilder().uncheckedBuild());
    attribute.getAttributeCardinality().setOptional(isOptional);
    attribute.getAttributeCardinality().setOne(!isOptional);
    return attribute;
  }

  protected ASTExpression createPrimaryExpressionForString(String variableName) {
    ASTStringLiteral stringLiteral =  ODRulesMill.stringLiteralBuilder().uncheckedBuild();
    stringLiteral.setSource(variableName);
    return ExpressionsBasisMill.literalExpressionBuilder().setLiteral(stringLiteral).build();
  }

  protected void createQualifiedNameExpressionAsAttributeValue(ASTODAttribute attribute,
      String... parts) {
    List<String> stringList = Lists.newArrayList(parts);
    ASTExpression qualifiedNameExpression = createQualifiedNameExpression(stringList);
    attribute.setSingleValue(qualifiedNameExpression);
  }


}
